#ifndef HALIDE_ERROR_H
#define HALIDE_ERROR_H

#include <memory>
#include <sstream>
#include <stdexcept>

#include "Debug.h"
#include "runtime/HalideRuntime.h"  // for HALIDE_ALWAYS_INLINE

namespace Halide {

/** Query whether Halide was compiled with exceptions. */
bool exceptions_enabled();

/** A base class for Halide errors.
 *
 * Note that this deliberately does *not* descend from std::runtime_error, or
 * even std::exception; unfortunately, std::runtime_error is not marked as
 * DLLEXPORT on Windows, but Error needs to be marked as such, and mismatching
 * DLLEXPORT annotations in a class inheritance hierarchy in this way can lead
 * to ODR violations. Instead, we just attempt to replicate the API of
 * runtime_error here. */
struct HALIDE_EXPORT_SYMBOL Error {
    Error() = delete;

    // Give each class a non-inlined constructor so that the type
    // doesn't get separately instantiated in each compilation unit.
    explicit Error(const char *msg);
    explicit Error(const std::string &msg);

    Error(const Error &);
    Error &operator=(const Error &);
    Error(Error &&) noexcept;
    Error &operator=(Error &&) noexcept;

    virtual ~Error();

    virtual const char *what() const noexcept;

private:
    // Using a std::string here will cause MSVC to complain about the fact
    // that class std::string isn't declared DLLEXPORT, even though the
    // field is private; rather than suppress the warning, we'll just use
    // an old-fashioned new-and-delete to keep it nice and clean.
    char *what_;
};

/** An error that occurs while running a JIT-compiled Halide pipeline. */
struct HALIDE_EXPORT_SYMBOL RuntimeError final : Error {
    static constexpr auto verbose_name = "Runtime error";
    static constexpr int verbose_debug_level = 1;

    explicit RuntimeError(const char *msg);
    explicit RuntimeError(const std::string &msg);
};

/** An error that occurs while compiling a Halide pipeline that Halide
 * attributes to a user error. */
struct HALIDE_EXPORT_SYMBOL CompileError final : Error {
    static constexpr auto verbose_name = "User error";
    static constexpr int verbose_debug_level = 1;

    explicit CompileError(const char *msg);
    explicit CompileError(const std::string &msg);
};

/** An error that occurs while compiling a Halide pipeline that Halide
 * attributes to an internal compiler bug, or to an invalid use of
 * Halide's internals. */
struct HALIDE_EXPORT_SYMBOL InternalError final : Error {
    static constexpr auto verbose_name = "Internal error";
    static constexpr int verbose_debug_level = 0;  // Always print location/condition info

    explicit InternalError(const char *msg);
    explicit InternalError(const std::string &msg);
};

/** CompileTimeErrorReporter is used at compile time (*not* runtime) when
 * an error or warning is generated by Halide. Note that error() is called
 * a fatal error has occurred, and returning to Halide may cause a crash;
 * implementations of CompileTimeErrorReporter::error() should never return.
 * (Implementations of CompileTimeErrorReporter::warning() may return but
 * may also abort(), exit(), etc.)
 */
class CompileTimeErrorReporter {
public:
    virtual ~CompileTimeErrorReporter() = default;
    virtual void warning(const char *msg) = 0;
    [[noreturn]] virtual void error(const char *msg) = 0;
};

/** The default error reporter logs to stderr, then throws an exception
 * (if HALIDE_WITH_EXCEPTIONS) or calls abort (if not). This allows customization
 * of that behavior if a more gentle response to error reporting is desired.
 * Note that error_reporter is expected to remain valid across all Halide usage;
 * it is up to the caller to ensure that this is the case (and to do any
 * cleanup necessary).
 */
void set_custom_compile_time_error_reporter(CompileTimeErrorReporter *error_reporter);

namespace Internal {

/**
 * If a custom error reporter is configured, notifies the reporter by calling
 * its error() function with the value of \p e.what()
 *
 * Otherwise, if Halide was built with exceptions, throw \p e unless an
 * existing exception is in flight. On the other hand, if Halide was built
 * without exceptions, print the error message to stderr and abort().
 *
 * @param e The error to throw or report
 */
/// @{
[[noreturn]] void throw_error(const RuntimeError &e);
[[noreturn]] void throw_error(const CompileError &e);
[[noreturn]] void throw_error(const InternalError &e);
/// @}

/**
 * If a custom error reporter is configured, notifies the reporter by calling
 * its warning() function. Otherwise, prints the warning to stderr.
 *
 * @param warning The warning to issue
 */
void issue_warning(const char *warning);

template<typename T>
class ReportBase {
    struct Contents {
        std::ostringstream msg{};
        bool finalized{false};
    };
    std::unique_ptr<Contents> contents = std::make_unique<Contents>();

public:
    template<typename S>
    HALIDE_ALWAYS_INLINE T &operator<<(const S &x) {
        contents->msg << x;
        return *static_cast<T *>(this);
    }

    HALIDE_ALWAYS_INLINE operator bool() const {
        return !contents->finalized;
    }

protected:
    // This function is called as part of issue() below. We can't use a
    // virtual function because issue() needs to be marked [[noreturn]]
    // for errors and be left alone for warnings (i.e., they have
    // different signatures).
    std::string finalize_message() {
        if (!contents->msg.str().empty() && contents->msg.str().back() != '\n') {
            contents->msg << "\n";
        }
        contents->finalized = true;
        return contents->msg.str();
    }

    T &init(const char *file, const char *function, const int line, const char *condition_string) {
        if (debug_is_active_impl(T::verbose_debug_level, file, function, line)) {
            contents->msg << T::verbose_name << " at " << file << ":" << line << '\n';
            if (condition_string) {
                contents->msg << "Condition failed: " << condition_string << '\n';
            }
        }
        return *static_cast<T *>(this);
    }
};

template<typename Exception>
struct ErrorReport final : ReportBase<ErrorReport<Exception>> {
    static constexpr auto verbose_name = Exception::verbose_name;
    static constexpr int verbose_debug_level = Exception::verbose_debug_level;

    ErrorReport &init(const char *file, const char *function, const int line, const char *condition_string) {
        return ReportBase<ErrorReport>::init(file, function, line, condition_string) << "Error: ";
    }

    [[noreturn]] void issue() noexcept(false) {
        throw_error(Exception(this->finalize_message()));
    }
};

struct WarningReport final : ReportBase<WarningReport> {
    static constexpr auto verbose_name = "Warning";
    static constexpr int verbose_debug_level = 1;

    WarningReport &init(const char *file, const char *function, const int line, const char *condition_string) {
        return ReportBase::init(file, function, line, condition_string) << "Warning: ";
    }

    void issue() {
        issue_warning(this->finalize_message().c_str());
    }
};

/**
 * The following three diagnostic macros are implemented such that the
 * message is evaluated only if the assertion's value is false.
 *
 * This (regrettably) requires a macro to work, but has the highly desirable
 * effect that all assertion parameters are totally skipped (not ever evaluated)
 * when the assertion is true.
 *
 * The macros work by deferring the call to issue() until after the stream
 * has been evaluated. This previously used a trick where ErrorReport would
 * throw in the destructor, but throwing in a destructor is UB in a lot of
 * scenarios, and it was easy to break things by mistake.
 */
/// @{
#define _halide_error_impl(type)                                    \
    for (Halide::Internal::ErrorReport<type> _err; 1; _err.issue()) \
    /**/ _err.init(__FILE__, __FUNCTION__, __LINE__, nullptr)

#define _halide_assert_impl(condition, type)                            \
    if (!(condition))                                                   \
        for (Halide::Internal::ErrorReport<type> _err; 1; _err.issue()) \
    /*****/ _err.init(__FILE__, __FUNCTION__, __LINE__, #condition)

#define _halide_user_warning                                       \
    for (Halide::Internal::WarningReport _err; _err; _err.issue()) \
    /**/ _err.init(__FILE__, __FUNCTION__, __LINE__, nullptr)
/// @}

#define user_warning _halide_user_warning

#define user_error _halide_error_impl(Halide::CompileError)
#define internal_error _halide_error_impl(Halide::InternalError)
#define halide_runtime_error _halide_error_impl(Halide::RuntimeError)

#define internal_assert(c) _halide_assert_impl(c, Halide::InternalError)
#define user_assert(c) _halide_assert_impl(c, Halide::CompileError)

// The nicely named versions get cleaned up at the end of Halide.h,
// but user code might want to do halide-style user_asserts (e.g. the
// Extern macros introduce calls to user_assert), so for that purpose
// we define an equivalent macro that can be used outside of Halide.h
#define _halide_user_error _halide_error_impl(Halide::CompileError)
#define _halide_internal_error _halide_error_impl(Halide::InternalError)
#define _halide_runtime_error _halide_error_impl(Halide::RuntimeError)
#define _halide_internal_assert(c) _halide_assert_impl(c, Halide::InternalError)
#define _halide_user_assert(c) _halide_assert_impl(c, Halide::CompileError)

// N.B. Any function that might throw a user_assert or user_error may
// not be inlined into the user's code, or the line number will be
// misattributed to Halide.h. Either make such functions internal to
// libHalide, or mark them as HALIDE_NO_USER_CODE_INLINE.

// handler suitable for use with std::terminate; it will catch unhandled exceptions
// and log the `what()` to stderr, then abort. Exposed as a function to minimize
// the need for external code to need to know the definition of Halide::Error.
HALIDE_EXPORT_SYMBOL void unhandled_exception_handler();

}  // namespace Internal

}  // namespace Halide

#endif
